using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;

namespace NetworkPong
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class NetworkPong : Microsoft.Xna.Framework.Game
    {
        const int maxGamers = 2;
        const int maxLocalGamers = 1;

        private GraphicsDeviceManager graphics;
        private SpriteBatch spriteBatch;

        private SpriteFont font;

        private Ball ball;

        private KeyboardState kstate;
        private GamePadState gstate;

        NetworkSession networkSession;

        PacketWriter packetWriter = new PacketWriter();
        PacketReader packetReader = new PacketReader();

        String errorMessage;

        public NetworkPong()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            Components.Add(new GamerServicesComponent(this));
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            ball = new Ball(Content, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height);

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            font = Content.Load<SpriteFont>("Font");
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            HandleInput();

            // Check to see if there is a network session active already
            if (networkSession == null)
            {
                UpdateMenuScreen();
            }
            else
            {
                UpdateNetworkSession();
            }

            base.Update(gameTime);
        }

        // Sign the player in and display the menu
        private void UpdateMenuScreen()
        {
            if (IsActive)
            {
                // Sign player in if there's no players signed in
                if (Gamer.SignedInGamers.Count == 0)
                {
                    Guide.ShowSignIn(maxLocalGamers, false);
                }
                else if (IsPressed(Keys.A, Buttons.A))
                {
                    CreateSession();
                }
                else if (IsPressed(Keys.B, Buttons.B))
                {
                    JoinSession();
                }
            }
        }

        // Attempts to create a session
        private void CreateSession()
        {
            DrawMessage("Creating game...");

            try
            {
                networkSession = NetworkSession.Create(NetworkSessionType.SystemLink, maxLocalGamers, maxGamers);

                HookSessionEvents();
            }
            catch (Exception e)
            {
                errorMessage = e.Message;
            }
        }

        // Attempts to join a sesison
        private void JoinSession()
        {
            DrawMessage("Joining game...");

            try
            {
                using (AvailableNetworkSessionCollection availableSessions = NetworkSession.Find(NetworkSessionType.SystemLink, maxLocalGamers, null))
                {
                    if (availableSessions.Count == 0)
                    {
                        errorMessage = "No network session found";
                        return;
                    }

                    networkSession = NetworkSession.Join(availableSessions[0]);

                    HookSessionEvents();
                }
            }
            catch (Exception e)
            {
                errorMessage = e.Message;
            }
        }

        private void HookSessionEvents()
        {
            networkSession.GamerJoined += GamerJoinedEventHandler;
            networkSession.SessionEnded += SessionEndedEventHandler;
        }

        // Added a new paddle to the gamer that just joined
        private void GamerJoinedEventHandler(object sender, GamerJoinedEventArgs e)
        {
            int gamerIndex = networkSession.AllGamers.IndexOf(e.Gamer);

            e.Gamer.Tag = new Paddle(gamerIndex, Content, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height);
        }

        // Handles when a session has ended
        private void SessionEndedEventHandler(object sender, NetworkSessionEndedEventArgs e)
        {
            errorMessage = e.EndReason.ToString();

            networkSession.Dispose();
            networkSession = null;
        }

        // Update a session
        private void UpdateNetworkSession()
        {
            // Updates the local player
            foreach (LocalNetworkGamer gamer in networkSession.LocalGamers)
            {
                UpdateLocalGamer(gamer);
            }

            // Tries to update the session
            networkSession.Update();

            if (networkSession == null)
            {
                return;
            }

            // Reads the incoming information
            foreach (LocalNetworkGamer gamer in networkSession.LocalGamers)
            {
                ReadIncomingPackets(gamer);
            }

            // If current play is the host, update the ball
            if (networkSession.IsHost)
                ball.Update();
        }

        // Update the local paddle and broadcast the local paddle and ball positions to the remote player
        private void UpdateLocalGamer(LocalNetworkGamer gamer)
        {
            Paddle localPaddle = gamer.Tag as Paddle;

            ReadPaddleInput(localPaddle, gamer.SignedInGamer.PlayerIndex);

            localPaddle.Update();

            if (localPaddle.BB.Intersects(ball.BB))
                ball.Flip();

            packetWriter.Write(localPaddle.Position);
            packetWriter.Write(ball.Position);

            gamer.SendData(packetWriter, SendDataOptions.InOrder);
        }

        // Read information sent from the other player
        private void ReadIncomingPackets(LocalNetworkGamer gamer)
        {
            while (gamer.IsDataAvailable)
            {
                NetworkGamer sender;

                gamer.ReceiveData(packetReader, out sender);

                if (sender.IsLocal)
                    continue;

                Paddle remotePaddle = sender.Tag as Paddle;

                // Read the remote player's paddle information
                remotePaddle.Position = packetReader.ReadVector2();

                // Ignore the incoming ball position if it's sent by the remote player
                // Only want the host updating the ball position
                if (!networkSession.IsHost)
                {
                    ball.Position = packetReader.ReadVector2();
                }

                // Bounce the ball when it hits the remote paddle
                if(networkSession.IsHost)
                    if (remotePaddle.BB.Intersects(ball.BB))
                        ball.Flip();
            }
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            if (networkSession == null)
            {
                DrawMenuScreen();
            }
            else
            {
                DrawNetworkSession();                
            }

            base.Draw(gameTime);
        }

        private void DrawMenuScreen()
        {
            String message = String.Empty;

            if (!String.IsNullOrEmpty(errorMessage))
                message += "Error:\n" + errorMessage.Replace(".", ".\n") + "\n\n";

            message += "Press A to create a game \n" +
                       "Press B to join a game";

            spriteBatch.Begin();

            spriteBatch.DrawString(font, message, new Vector2(50, 300), Color.Black);

            spriteBatch.End();
        }

        private void DrawNetworkSession()
        {
            spriteBatch.Begin();

            foreach (NetworkGamer gamer in networkSession.AllGamers)
            {
                Paddle paddle = gamer.Tag as Paddle;
                
                paddle.Draw(spriteBatch);
            }

            ball.Draw(spriteBatch);

            spriteBatch.End();
        }

        private void DrawMessage(String message)
        {
            if (!BeginDraw())
                return;

            GraphicsDevice.Clear(Color.CornflowerBlue);

            spriteBatch.Begin();

            spriteBatch.DrawString(font, message, new Vector2(50, 250), Color.Black);

            spriteBatch.End();

            EndDraw();
        }

        private void HandleInput()
        {
            kstate = Keyboard.GetState();
            gstate = GamePad.GetState(PlayerIndex.One);
            
            if (IsActive && IsPressed(Keys.Escape, Buttons.Back))
                this.Exit();
        }

        bool IsPressed(Keys key, Buttons button)
        {
            return (kstate.IsKeyDown(key) || gstate.IsButtonDown(button));
        }

        private void ReadPaddleInput(Paddle paddle, PlayerIndex playerIndex)
        {
            GamePadState gamePad = GamePad.GetState(playerIndex);
            KeyboardState keyboard = Keyboard.GetState(playerIndex);

            if (keyboard.IsKeyDown(Keys.Up) || gamePad.IsButtonDown(Buttons.LeftThumbstickUp) || gamePad.IsButtonDown(Buttons.DPadUp))
            {
                paddle.Up();
            }
            else if (keyboard.IsKeyDown(Keys.Down) || gamePad.IsButtonDown(Buttons.LeftThumbstickDown) || gamePad.IsButtonDown(Buttons.DPadDown))
            {
                paddle.Down();
            }

            if ((keyboard.IsKeyDown(Keys.Space) || gamePad.IsButtonDown(Buttons.Start)) && networkSession.AllGamers.Count > 1)
            {
                ball.IsActive = true;
            }
        }
    }
}
